package com.crime.crimemonitorclient;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.NavigableSet;
import java.util.TreeSet;

import android.graphics.Color;

import com.androidplot.ui.YLayoutStyle;
import com.androidplot.ui.YPositionMetric;
import com.androidplot.xy.BoundaryMode;
import com.androidplot.xy.SimpleXYSeries;
import com.androidplot.xy.XValueMarker;
import com.androidplot.xy.XYPlot;
import com.androidplot.xy.XYSeries;
import com.androidplot.xy.XYSeriesFormatter;
import com.androidplot.xy.SimpleXYSeries.ArrayFormat;

public abstract class TimeSeriesManager
	implements OnCrimeObservationsLoadedListener
{	
	private OnCrimeObservationsLoadedListener mCrimeObservationsLoadedListener;
	private XYPlot mPlot;
	private CrimeObservationsLoader mLoader;
	
	protected XYSeries mTimeSeries;
	protected TreeSet<CrimeTimeSeriesObservation> mObservations;
	
	protected List<CrimeObservationsTimeSeries> mObservationSeriesList;
	
	/// TODO
	protected XYSeriesFormatter mTimeSeriesFormatter;
	
	protected TimeGranularity mTimeGranularity;
	
	protected long mMinTime;
	protected long mMaxTime;
	
	protected DateFormat mTimeMarkerFormatter;
	protected DateFormat mTimeMarkerEndPointFormatter;
	
	private final int TIME_MARKER_VERTICAL_OFFSET = 10;
	private final YLayoutStyle TIME_MARKER_LAYOUT_STYLE = YLayoutStyle.ABSOLUTE_FROM_BOTTOM;
	private final int TIME_MARKER_LINE_COLOR = Color.TRANSPARENT;
	private final int TIME_MARKER_TEXT_COLOR = Color.YELLOW;
	
	private final int TIME_ENDPOINT_MARKER_VERTICAL_OFFSET = 20;
	private final YLayoutStyle TIME_ENDPOINT_MARKER_LAYOUT_STYLE = YLayoutStyle.ABSOLUTE_FROM_BOTTOM;
	private final int TIME_ENDPOINT_MARKER_LINE_COLOR = Color.TRANSPARENT;
	private final int TIME_ENDPOINT_MARKER_TEXT_COLOR = Color.YELLOW;
	
	public void attach(final XYPlot plot, final CrimeObservationsLoader loader,
			final OnCrimeObservationsLoadedListener crimeObservationsLoadedListener)
	{
		if(plot == null)
			throw new NullPointerException("Argument 'plot' cannot be null.");
		
		if(loader == null)
			throw new NullPointerException("Argument 'loader' cannot be null.");
		
		if(crimeObservationsLoadedListener == null)
		throw new NullPointerException("Argument 'crimeObservationsLoadedListener' cannot be null.");
		
		this.mCrimeObservationsLoadedListener = crimeObservationsLoadedListener;
		this.mPlot = plot;
		this.mLoader = loader;
		
		// add all managed time series to the plot
		this.mPlot.addSeries(this.mTimeSeries, this.mTimeSeriesFormatter);
	}
	
	public void init()
	{
		this.initTimeGranularity();
		this.initTimeSeries();
		this.initTimeSeriesFormatter();
		this.initTimeMarkerFormatters();
	}
	
	public void detach()
	{
		// add all managed time series to the plot
		this.mPlot.removeSeries(this.mTimeSeries);
		
		// store reference to a given plot
		this.mPlot = null;
	}
	
	protected abstract void loadObservations();
	public abstract long getMinimumDisplayTimeRange();
	public abstract long getMaximumDisplayTimeRange();
	private void initTimeSeries() {
		SimpleXYSeries thisSeries = new SimpleXYSeries("");
		this.mTimeSeries = thisSeries;
		this.mObservations = new TreeSet<CrimeTimeSeriesObservation>(new Comparator<CrimeTimeSeriesObservation>() {

			@Override
			public int compare(CrimeTimeSeriesObservation lhs,
					CrimeTimeSeriesObservation rhs) {
				
				return Long.valueOf(lhs.getObsTime()).compareTo(rhs.getObsTime());
			}
			
		});
	}
	protected abstract void initTimeGranularity();
	protected abstract void initTimeSeriesFormatter();
	private String formatObservationTime(Date obsDateTime)
	{
		return this.mTimeMarkerFormatter.format(obsDateTime);
	}
	private String formatTimeRangeEndPoint(Date endPointDateTime)
	{
		return this.mTimeMarkerEndPointFormatter.format(endPointDateTime);
	}
	private void initTimeMarkerFormatters()
	{
		this.mTimeMarkerFormatter = new SimpleDateFormat(this.getTimeMarkerFormattingString());
		this.mTimeMarkerEndPointFormatter = new SimpleDateFormat(this.getTimeMarkerEndPointFormattingString());
	}
	protected abstract String getTimeMarkerFormattingString();
	protected abstract String getTimeMarkerEndPointFormattingString();
	
	/// TODO: synchronize concurrent access?
	public void beginRedraw(long minTime, long maxTime)
	{
		// 1) compute requested data range according to the data already loaded
		long minTimeActuallyNeeded = minTime;
		long maxTimeActuallyNeeded = maxTime;
		
		// 2) if there is need to load data, start loading it
		if(minTimeActuallyNeeded != maxTimeActuallyNeeded)
		{
			this.mLoader.beginLoad(minTimeActuallyNeeded, maxTimeActuallyNeeded, this.mTimeGranularity, this);
		}
		// 3) if the data is already present, draw it
		else
		{
			this.draw(minTime, maxTime);
		}
	}

	private void draw(long minTime, long maxTime) {
		// remove all currently set markers
		this.mPlot.removeXMarkers();
		
		Calendar calendar = Calendar.getInstance();
		calendar.clear();
		
		// get observations in given time range
		NavigableSet<CrimeTimeSeriesObservation>  observationsInTimeRange =
				this.mObservations.subSet(
				new CrimeTimeSeriesObservation(minTime, CrimeTimeSeriesObservation.INVALID_OBS_VALUE), true,
				new CrimeTimeSeriesObservation(maxTime, CrimeTimeSeriesObservation.INVALID_OBS_VALUE), true);
		
		// draw markers for those
		for(CrimeTimeSeriesObservation observationInTimeRange : observationsInTimeRange)
		{
			long obsTime = observationInTimeRange.getObsTime();
			calendar.setTimeInMillis(obsTime);
			this.mPlot.addMarker(
					new XValueMarker(
							obsTime,
							this.formatObservationTime(calendar.getTime()),
							new YPositionMetric(TIME_MARKER_VERTICAL_OFFSET, TIME_MARKER_LAYOUT_STYLE),
							TIME_MARKER_LINE_COLOR, TIME_MARKER_TEXT_COLOR));
		}
		
		// draw border markers displaying a surrounding unit of lower granularity
		// 1) the left end
		calendar.setTimeInMillis(minTime);
		this.mPlot.addMarker(
				new XValueMarker(
						minTime,
						this.formatTimeRangeEndPoint(calendar.getTime()),
						new YPositionMetric(TIME_ENDPOINT_MARKER_VERTICAL_OFFSET, TIME_ENDPOINT_MARKER_LAYOUT_STYLE),
						TIME_ENDPOINT_MARKER_LINE_COLOR, TIME_ENDPOINT_MARKER_TEXT_COLOR));
		
		// 2) the right end
		calendar.setTimeInMillis(maxTime);
				this.mPlot.addMarker(
						new XValueMarker(
								maxTime,
								this.formatTimeRangeEndPoint(calendar.getTime()),
								new YPositionMetric(TIME_ENDPOINT_MARKER_VERTICAL_OFFSET, TIME_ENDPOINT_MARKER_LAYOUT_STYLE),
								TIME_ENDPOINT_MARKER_LINE_COLOR, TIME_ENDPOINT_MARKER_TEXT_COLOR));

		this.mMinTime = minTime;
		this.mMaxTime = maxTime;
		
		this.mPlot.setDomainLowerBoundary(this.mMinTime, BoundaryMode.FIXED);
		this.mPlot.setDomainUpperBoundary(this.mMaxTime, BoundaryMode.FIXED);
	}
	
	public void onCrimeObservationsLoaded(CrimeTimeSeriesObservation[] loadedObservations, long minTime, long maxTime)
	{
		// TODO: display a message from a passed exception
		if(loadedObservations == null)
			return;
		
		// merge the loaded data with existing observations
		this.mergeLoadedObservations(loadedObservations);
		
		// draw the returned data
		this.draw(minTime, maxTime);
		
		// call the listener for observations loading
		this.mCrimeObservationsLoadedListener.onCrimeObservationsLoaded(loadedObservations, minTime, maxTime);
	}

	private void mergeLoadedObservations(CrimeTimeSeriesObservation[] loadedObservations)
	{
		this.mObservations.addAll(Arrays.asList(loadedObservations));
		
		// set the merged data as a model for the current series
		SimpleXYSeries thisSeries = (SimpleXYSeries)this.mTimeSeries;
		thisSeries.setModel(this.getObservationsAsTimeAndValueInterleaved(), ArrayFormat.XY_VALS_INTERLEAVED);
	}

	private List<Number> getObservationsAsTimeAndValueInterleaved()
	{
		List<Number> timeValueList = new ArrayList<Number>(2 * this.mObservations.size());
		for(CrimeTimeSeriesObservation observation : this.mObservations)
		{
			timeValueList.add(observation.getObsTime());
			timeValueList.add(observation.getObsValue());
		}
		
		return timeValueList;
	}
}













